#!/usr/bin/php
<?php
/*
 * Copyright (c) 2010 Andrei "taviscaron" Senchuk
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

declare(ticks = 1); // need for signals
error_reporting (E_ALL);

/**
 * author: Andrei U. Senchuk
 * desc: simple file exchange http server
 */
class httpd
{
	/*
	 * server name, version;
	 */
	private static $server = 'phphttpserver';
	private static $version = '0.0.1';

	/*
	 * MIME types. Script use this assoc. array, if
	 * function mime_content_type() unavailable
	 * 'file_extension' => 'mime_type'
	 */
	private static $mimes = array (
			'txt'	=> 'text/plain',
			'htm'	=> 'text/html',
			'html'	=> 'text/html',
			'php'	=> 'text/html',
			'css'	=> 'text/css',
			'js'	=> 'application/javascript',
			'json'	=> 'application/json',
			'xml'	=> 'application/xml',
			'swf'	=> 'application/x-shockwave-flash',
			'flv'	=> 'video/x-flv',
	
			/* images */
			'png'	=> 'image/png',
			'jpe'	=> 'image/jpeg',
			'jpeg'	=> 'image/jpeg',
			'jpg'	=> 'image/jpeg',
			'gif'	=> 'image/gif',
			'bmp'	=> 'image/bmp',
			'ico'	=> 'image/vnd.microsoft.icon',
			'tiff'	=> 'image/tiff',
			'tif'	=> 'image/tiff',
			'svg'	=> 'image/svg+xml',
			'svgz'	=> 'image/svg+xml',
	
			/* archives */
			'zip'	=> 'application/zip',
			'rar'	=> 'application/x-rar-compressed',
			'exe'	=> 'application/x-msdownload',
			'msi'	=> 'application/x-msdownload',
			'cab'	=> 'application/vnd.ms-cab-compressed',
			'zip'	=> 'application/zip',
			'bz2'	=> 'application/x-bzip2',
			'gz'	=> 'application/x-gzip',
	
			/* audio/video */
			'mp3'	=> 'audio/mpeg',
			'avi'	=> 'video/avi',
			'qt'	=> 'video/quicktime',
			'mov'	=> 'video/quicktime',
	
			/* adobe */
			'pdf'	=> 'application/pdf',
			'psd'	=> 'image/vnd.adobe.photoshop',
			'ai'	=> 'application/postscript',
			'eps'	=> 'application/postscript',
			'ps'	=> 'application/postscript',
	
			/* office */
			'doc'	=> 'application/msword',
			'rtf'	=> 'application/rtf',
			'xls'	=> 'application/vnd.ms-excel',
			'ppt'	=> 'application/vnd.ms-powerpoint',
			'odt'	=> 'application/vnd.oasis.opendocument.text',
			'ods'	=> 'application/vnd.oasis.opendocument.spreadsheet',
		);

	/*
	 * Error code with error msg
	 * 'errno' => 'error_message'
	 */
	private static $error_messages = array (
				404	=> '<h1>Error 404</h1>Not Found',
				403	=> '<h1>Error 403</h1>Forbidden',
				400	=> '<h1>Error 400</h1>Bad request',
					);

	/*
	 * regs for http command, header and uri
	 */
	private static $command_reg = "/^(GET|HEAD) ([^ ]*) HTTP\/(\d.\d)/";
	private static $headers_reg = "/([^ \n\r]*): (.*)/";
	private static $uri_reg = "/^http:\/\/[^ \/]*\/([^ ]*)$/";

	/*
	 * Listen socket
	 */
	private $socket;

	/*
	 * Listen port
	 */
	private $port = 8080;

	/*
	 * use 0.0.0.0 for listening on all of available interfaces
	 */
	private $addr = '0.0.0.0';

	/*
	 * root directory
	 */
	private $root;

	public function __construct ($addr, $port, $root)
	{
		$this->addr = $addr;
		$this->port = $port;
		$this->root = $root;

		if(($this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) === false)
		{
			throw new Exception ('socket error '.socket_last_error().': '.socket_strerror(socket_last_error()));
		}

		if(@socket_bind($this->socket, $this->addr, $this->port) === false)
		{
			throw new Exception ('socket error '.socket_last_error().': '.socket_strerror(socket_last_error()));
		}

		if(@socket_listen ($this->socket) === false)
		{
			throw new Exception ('socket error '.socket_last_error().': '.socket_strerror(socket_last_error()));
		}

		if(!pcntl_signal(SIGCHLD, array($this, 'sig_chld')))
		{
			throw new Exception ("can't set sig_handlers\n");
		}


		while(1)
		{
			if(($client = @socket_accept ($this->socket)) === false)
			{
				throw new Exception (socket_last_error().': '.socket_strerror(socket_last_error()));
			}

			//socket_getpeername ($client, $client_addr, $client_port);

			$pid = pcntl_fork();
			
			if ($pid == -1)
			{
				throw new Exception ('fork failed');
			}
			else if ($pid > 0)
			{
				@socket_close ($client);
			}
			else
			{
				@socket_close ($this->socket);

				$this->client_thread ($client);
				@socket_shutdown($client, 2);
				@socket_close ($client);
				exit ();
			}
		}
	}

	private function client_thread ($socket)
	{
		$request = $this->http_request ($socket);

		if($request === false)
		{
			$this->http_answer ($socket, array ('req' => $request, 'code' => 400));
		}
		else
		{
			if($request['uri'] === false)
			{
				$this->http_answer ($socket, array ('req' => $request, 'code' => 404));
			}
			else if(!$this->uri_check_permission ($request['uri']))
			{
				$this->http_answer ($socket, array ('req' => $request, 'code' => 403));
			}
			else
			{
				$this->http_answer ($socket, array ('req' => $request, 'code' => 200));
			}
		}
	}

	/*
	 * return false, if request is wrong, and
	 * method, file, http version and headers
	 */
	private function http_request ($socket)
	{
		$request = '';

		while(strpos($request, "\r\n\r\n") === false)
		{
			$buffer = socket_read ($socket, 2048, PHP_BINARY_READ);

			if($buffer === false)
			{
				break;
			}

			$request .= $buffer;
		}

		if(preg_match (self::$command_reg, $request, $req_matches) != 1)
		{
			return false;
		}

		$method = $req_matches[1];
		$uri = $req_matches[2];
		/*$version = $req_matches[3];*/

		/* URL decode */
		$uri = urldecode ($uri);

		if(preg_match (self::$uri_reg, $uri, $matches))
		{
			$uri = $matches[1];
		}

		$req = array (
				'method' => $method,
				'uri' => realpath($this->root.$uri),
				/*'version' => $version,*/
			);

		/* parsing headers */
		//preg_match_all (self::$headers_reg, $request, $matches);
		//
		//foreach ($matches[1] as $key => $header)
		//{
		//	$req['headers'][$header] = $matches[2][$key];
		//}

		return $req;
	}

	private function http_answer ($socket, $ans)
	{
		if(isset(self::$error_messages[$ans['code']]))
		{
			$content_type = 'text/html';
			$content_size = strlen(self::$error_messages[$ans['code']]);
			$a_type = 'error';
		}
		else if(is_dir($ans['req']['uri']))
		{
			$directory = $this->get_directory ($ans['req']['uri']);
			$content_type = 'text/html';
			$content_size = strlen($directory);
			$a_type = 'dir';
		}
		else if(is_file($ans['req']['uri']))
		{
			$content_type = $this->get_mime_type ($ans['req']['uri']);
			$content_size = filesize ($ans['req']['uri']);
			$a_type = 'file';
		}

		$headers = "HTTP/1.0 {$ans['code']}\r\n";
		$headers .= "Connection: close\r\n";
		$headers .= 'Server: '.self::$server.' '.self::$version."\r\n";
		$headers .= 'Data: '.date('r')."\r\n";
		$headers .= "Content-Type: {$content_type}\r\n";
		$headers .= "Content-Length: {$content_size}\r\n";
		$headers .= "\r\n";

		if(@socket_write ($socket, $headers) === false)
		{
			return;
		}

		if($ans['req']['method'] == 'HEAD')
		{
			/* header only */
			return;
		}

		switch ($a_type)
		{
			case 'error':
				@socket_write ($socket, self::$error_messages[$ans['code']]);
				break;
			case 'dir':
				@socket_write ($socket, $directory);
				break;
			case 'file':

				if(!($file = fopen ($ans['req']['uri'], 'rb')))
				{
					return;
				}

				flock ($file, LOCK_SH);

				while(!feof($file))
				{
					if(@socket_write ($socket, @fread($file, 5120)) === false)
					{
						break;
						
					}
				}

				flock ($file, LOCK_UN);
				fclose ($file);
				break;
		}
	}

	/*
	 * Generate directory listing
	 */
	private function get_directory ($path)
	{
		$index_of = $this->relate_path($path);

		$ret = "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\">\n";
		$ret .= "<html>\n<head>\n<title>Index of {$index_of}</title>\n";
		$ret .= "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n";
		$ret .= "<style type=\"text/css\">\n";
		$ret .= "body {background-color: #f5f5f5; font-size: 10pt;}\n";
		$ret .= "a {color: #2080B0; text-decoration: none; font-weight: bold;}\n";
		$ret .= "a:hover {color: red; text-decoration: underline;}\n";
		$ret .= "table.list {width: 700px; background-color: #fafafa; border: 1px dotted black;}\n";
		$ret .= "td.name {text-align: left;}\n";
		$ret .= "td.header {text-align: left;  border-bottom: 1px solid black; font-size: 15pt;}\n";
		$ret .= "td.footer {text-align: left; border-top: 1px solid black; }\n";
		$ret .= "tr.list_header {font-size: 15pt; font-weight: bold;}\n";
		$ret .= "td.lm {width:1px;}\ntd.size {width:50px;}\ntd.type {width:50px;}\n";
		$ret .= "</style>\n</head>\n<body>\n<center>\n<table class=\"list\">\n";
		$ret .= "<tr><td class=\"header\" colspan=\"4\">Index of <a href=\"{$index_of}\">{$index_of}</a></td></tr>\n";
		$ret .= "<tr class=\"list_header\"><td class=\"name\">Name</td><td class=\"type\">Type</td><td class=\"size\">Size</td><td class=\"lm\">Last&nbsp;Modified</td></tr>\n";

		$dir = dir($path);

		while(($file = $dir->read()) !== false)
		{
			if($file == '.')
			{
				continue;
			}

			$realpath = realpath($dir->path.'/'.$file);
			if($this->uri_check_permission ($realpath))
			{
				$files[] = array(
						'file' => (is_dir($realpath) ? $file.'/' : $file),
						'realpath' => $realpath,
						'filemtime' => date ('H:i:s\&\n\b\s\p\;d.m.Y', filemtime($realpath)),
						'filesize' => sprintf('%u', filesize($realpath)),
						'ext' => $this->get_file_extension ($realpath),
						'relate_path' => $this->relate_path($realpath),
					);
			}
		}

		$dir->close();

		usort($files, array($this, 'fname_cmp'));

		foreach($files as $file)
		{
			$ret .= "<tr><td class=\"name\"><a href=\"{$file['relate_path']}\">{$file['file']}</a></td>";
			$ret .= "<td class=\"type\">{$file['ext']}</td>";
			$ret .= "<td class=\"size\">{$file['filesize']}</td>";
			$ret .= "<td class=\"lm\">{$file['filemtime']}</td></tr>\n";
		}

		$ret .= "<tr><td class=\"footer\" colspan=\"4\">Page was generated by ".self::$server.' '.self::$version.' at '.date ('H:i:s\&\n\b\s\p\;d.m.Y')."</td></tr>\n";
		$ret .= "</table>\n</center>\n</body>\n</html>\n";

		return $ret;
	}

	/*
	 * Check file permission. root directory must be a part of $filename
	 * (with offset 0), and file must be available for reading
	 */

	private function uri_check_permission ($filename)
	{
		return strpos($filename, $this->root) === 0 && is_readable($filename);	
	}

	/*
	 * If function mime_content_type is available, then use it
	 */
	private function get_mime_type ($file)
	{
		$mime = '';
		if(function_exists('mime_content_type'))
		{
			$mime = mime_content_type($file);
		}
		else
		{
			$ext = $this->get_file_extension ($file);
			$mime = isset($ext) && isset(self::$mimes[$ext]) ? self::$mimes[$ext] : 'octet/stream';
		}
		return $mime;
	}

	private function get_file_extension ($file)
	{
		$ext = '';
		if(is_dir($file))
		{
			$ext = 'directory';
		}
		else
		{
			$path_parts = pathinfo($file);
			$ext = isset($path_parts['extension']) ? strtolower($path_parts['extension']) :  'unknown';
		}
		return $ext;
	}

	private function relate_path ($path)
	{
		$relate_path = str_replace ($this->root, '', $path);
		return $relate_path == '' ? '/' : $relate_path;
	}

	/*
	 * Sorting file list
	 */
	private function fname_cmp ($a, $b)
	{
		$order = 0;
		if($a['ext'] == $b['ext'])
		{
			$order = strcmp($a['file'], $b['file']);
		}
		else
		{
			$order =  ($a['ext'] == 'directory') ? -1 : 1;
		}
		return $order;
	}

	private function sig_chld ($signum)
	{
		$status = 0;
		do
		{
			$ret = pcntl_wait ($status, WNOHANG);
		}
		while ($ret != 0 && $ret != -1);
	}
}

try
{
	$opts = getopt ("uhba:p:d:");

	if (isset ($opts['u']) || isset ($opts['h']))
	{
		echo "Usage: ",$_SERVER['argv'][0]," [-b] [-a bind_addr] [-p bind_port] [-d root_directory]\n";
		exit ();
	}

	$bind_addr = (isset ($opts['a']) && $opts['a'] !== false) ? $opts['a'] : '0.0.0.0';
	$bind_port = (isset ($opts['p']) && $opts['p'] !== false) ? intval($opts['p']) : 8080;
	$root_directory = (isset ($opts['d']) && $opts['d'] !== false) ? $opts['d'] : getcwd ();
	$bg_mode = isset ($opts['b']);

	if ($bind_port < 1025 || $bind_port > 0xffff)
	{
		throw new Exception ('port must be an integer between 1025 and '.strval (0xffff));
	}

	if (!file_exists ($root_directory))
	{
		throw new Exception ('root directory is not exist');
	}
	else
	{
		$root_directory = realpath ($root_directory);
	}

	if ($bg_mode)
	{
		$pid = pcntl_fork();

		if($pid == -1)
		{
			throw new Exception ("fork failed");
		}
		else if($pid)
		{
			$f = @fopen(__FILE__.'.pid', 'w');
			@fwrite($f, $pid);
			@fclose($f);

			echo "Going to background mode...\n";
			exit (0);
		}

	}
	
	new httpd ($bind_addr, $bind_port, $root_directory);
}
catch (Exception $e)
{
	echo 'Error: '.$e->getMessage ()."\n";
}
